#region Copyright

// The contents of this file are subject to the Mozilla Public License
//  Version 1.1 (the "License"); you may not use this file except in compliance
//  with the License. You may obtain a copy of the License at
//  
//  http://www.mozilla.org/MPL/
//  
//  Software distributed under the License is distributed on an "AS IS"
//  basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
//  License for the specific language governing rights and limitations under 
//  the License.
//  
//  The Initial Developer of the Original Code is Robert Smyth.
//  Portions created by Robert Smyth are Copyright (C) 2008.
//  
//  All Rights Reserved.

#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Reflection;
using System.Threading;
using Alchemi.Core.Owner;
using NUnit.Extensions.NUnitGridRunner.Metrics;
using NUnitGridThreadRunner.Alchemi;


namespace NUnit.Extensions.NUnitGridRunner
{
    public class GridApplication : IDisposable
    {
        private readonly Stopwatch applicationExpiredTime = new Stopwatch();
        private readonly List<string> assembliesLoadedInManifest = new List<string>();
        private readonly TimeSpan expectedRunTime;
        private readonly List<string> modulesLoadedInManifest = new List<string>();
        private readonly NamedPipeServerStream pipe;
        private readonly int portNumber;
        private readonly GridBoxMetrics runMetrics;
        private readonly RunnerNames runnerNames;
        private readonly string server;
        private readonly ITextOutput textOutput;
        private readonly string workingDirectory;
        private bool applicationFinished;
        private int completedThreadsCount;
        private bool failed;
        private int failedThreads;
        private GApplication gridApplication;
        private int passedThreads;
        private StreamReader pipeStream;
        private TimeSpan totalExecutorRunTime = TimeSpan.Zero;

        public GridApplication(string server, int portNumber, string workingDirectory, ITextOutput textOutput,
                               TimeSpan expectedRunTime)
        {
            this.server = server;
            this.portNumber = portNumber;
            this.workingDirectory = workingDirectory;
            this.textOutput = textOutput;
            this.expectedRunTime = expectedRunTime;

            runMetrics = new GridBoxMetrics();
            runnerNames = new RunnerNames(new TargetFileSystem(workingDirectory));

            pipe = new NamedPipeServerStream("NUnitGridRunner", PipeDirection.In, 100, PipeTransmissionMode.Message,
                                             PipeOptions.Asynchronous);
            pipeStream = new StreamReader(pipe);
        }

        public void Dispose()
        {
            pipe.Dispose();
        }

        public bool Initialize()
        {
            pipe.BeginWaitForConnection(PipeConnection, this);

            gridApplication = null;
            try
            {
                var connection = new GConnection(server, portNumber, "user", "user");
                gridApplication = new GApplication(connection);
            }
            catch (Exception exception)
            {
                textOutput.Info.WriteLine(
                    "\nError connecting to host Alchecmi Manager on server '{0}' port {1}.\nException: '{2}'",
                    server, portNumber, exception.Message);
            }

            if (gridApplication != null)
            {
                gridApplication.ApplicationName = string.Format("NUnitGridRunner({0})", Environment.MachineName);
                gridApplication.ApplicationFinish += OnApplicationFinished;
                gridApplication.ThreadFinish += OnThreadFinish;

                gridApplication.Manifest.Add(new ModuleDependency(typeof (NUnitProcessRunner).Module));
            }
            else
            {
                failed = true;
            }

            var fileSystem = new TargetFileSystem(workingDirectory);
            fileSystem.Clean();

            return !failed;
        }

        private void PipeConnection(IAsyncResult result)
        {
            textOutput.Info.WriteLine("Client connected");
        }

        public bool Run(TimeSpan timeLimit)
        {
            if (!failed)
            {
                applicationExpiredTime.Start();

                gridApplication.Start();

                bool consoleApp = Environment.CommandLine.ToLower().StartsWith("nunitgridrunner");

                bool aborting = false;
                int numberOfThreads = gridApplication.Threads.Count;
                while (applicationExpiredTime.Elapsed < timeLimit && !applicationFinished &&
                       completedThreadsCount < numberOfThreads)
                {
                    Thread.Sleep(TimeSpan.FromMilliseconds(25));

                    if (!aborting && consoleApp && Console.KeyAvailable)
                    {
                        aborting = true;
                        new TargetFileSystem(workingDirectory).SetAbortFlag();
                        Console.WriteLine("Aborting ....");
                    }
                }
                textOutput.Info.WriteLine();

                failed |= passedThreads < numberOfThreads;

                textOutput.Info.WriteLine("  {0}\n\n", failed ? "== F A I L E D ==" : "== S U C C E S S ==");

                if (applicationExpiredTime.Elapsed >= timeLimit)
                {
                    failed = true;
                    textOutput.Info.WriteLine("Application timed out after {0:#,##0.#} minutes.",
                                              applicationExpiredTime.ElapsedMilliseconds/(60.0F*1000.0F));
                }
                else
                {
                    ShowSummary(numberOfThreads);
                }

                failed |= completedThreadsCount < numberOfThreads;

                runnerNames.Save();
            }

            return !failed;
        }

        private void ShowSummary(int numberOfThreads)
        {
            textOutput.Info.WriteLine("Ran on {0} network boxes.", runMetrics.GridBoxCount);
            textOutput.Info.WriteLine();
            textOutput.Info.WriteLine("Times");

            textOutput.Info.WriteLine("  Total expired time:  {0:#,##0} seconds.",
                                      applicationExpiredTime.ElapsedMilliseconds/1000.0F);

            textOutput.Info.WriteLine("  Grid processor time: {0:#,##0} seconds.",
                                      totalExecutorRunTime.TotalSeconds);

            textOutput.Info.WriteLine("  Expected run time:   {0:#,##0} seconds.",
                                      expectedRunTime.TotalSeconds);

            textOutput.Info.WriteLine();

            textOutput.Info.WriteLine(" Ran {0:0.0} times faster than if run locally (expired time / expected time).",
                                      expectedRunTime.TotalMilliseconds/applicationExpiredTime.ElapsedMilliseconds);

            textOutput.Info.WriteLine(
                " Ran {0:0.0} times faster on grid runners (total grid processor time / expected time).",
                expectedRunTime.TotalMilliseconds/totalExecutorRunTime.TotalMilliseconds);

            textOutput.Info.WriteLine();
            textOutput.Info.WriteLine("{0} grid runners: {1} passed, {2} Failed.", numberOfThreads,
                                      passedThreads,
                                      failedThreads);
        }

        private void OnApplicationFinished()
        {
            bool applicationFailed = completedThreadsCount != gridApplication.Threads.Count;
            failed |= applicationFailed;
            applicationFinished = true;

            var fileSystem = new TargetFileSystem(workingDirectory);
            fileSystem.Clean();
        }

        private void OnThreadFinish(GThread thread)
        {
            var gridRunner = (IGridRunner) thread;

            TimeSpan runTime = TimeSpan.FromSeconds(gridRunner.TotalRunTimeInSeconds);

            failed |= !gridRunner.Succeeded;
            totalExecutorRunTime += runTime;

            if (!gridRunner.Succeeded)
            {
                failedThreads++;
            }
            else
            {
                passedThreads++;
            }
            completedThreadsCount++;

            runMetrics.RegisterGridBox(gridRunner.ExecutorPC);

            string runnerName = runnerNames.MapName(gridRunner.ExecutorPC);

            textOutput.Info.WriteLine(
                "Runner on {3,-23} {1,2} of {2} completed. {0,5:#,##0.0} seconds.",
                runTime.TotalSeconds,
                completedThreadsCount,
                gridApplication.Threads.Count,
                string.Format("{0} {1}.",
                              runnerName,
                              !gridRunner.Succeeded ? "FAILED" : "passed"));

            if (thread is INUnitGridRunner)
            {
                var testRunner = (INUnitGridRunner) thread;

                ShowFixtureRunTimes(testRunner);
                textOutput.Info.WriteLine();
            }

            if (textOutput.Verbose.Enabled || !gridRunner.Succeeded)
            {
                textOutput.Info.WriteLine(gridRunner.Output);
            }
        }

        private void ShowFixtureRunTimes(INUnitGridRunner testRunner)
        {
            for (int fixtureIndex = 0; fixtureIndex < testRunner.Fixtures.Length; fixtureIndex++)
            {
                float fixtureRunTimeInSeconds = testRunner.FixtureTimesInSeconds[fixtureIndex];
                string fixtureName = testRunner.Fixtures[fixtureIndex];

                string runTime;
                if (float.IsNaN(fixtureRunTimeInSeconds))
                {
                    runTime = "Skipped";
                }
                else
                {
                    runTime = string.Format("{0,7:#,##0.##0} seconds", fixtureRunTimeInSeconds);
                }
                textOutput.Info.WriteLine("  {0}. {1}", runTime, fixtureName);
            }
        }

        public void AddThread(GThread runner)
        {
            gridApplication.Threads.Add(runner);
        }

        public void AddAssemblyToManifest(Assembly assemblyToLoad)
        {
            if (!assembliesLoadedInManifest.Contains(assemblyToLoad.FullName))
            {
                assembliesLoadedInManifest.Add(assemblyToLoad.FullName);
                foreach (Module module in assemblyToLoad.GetModules())
                {
                    if (!modulesLoadedInManifest.Contains(module.FullyQualifiedName))
                    {
                        modulesLoadedInManifest.Add(module.FullyQualifiedName);
                        gridApplication.Manifest.Add(new ModuleDependency(module));
                    }
                }
                LoadReferencedModules(assemblyToLoad);
            }
        }

        private void LoadReferencedModules(Assembly assemblyToLoad)
        {
            foreach (AssemblyName assemblyName in assemblyToLoad.GetReferencedAssemblies())
            {
                string name = assemblyName.Name;
                if (name.Split('.')[0] != "System" && name != "mscorlib")
                {
                    Assembly referencedAssembly;

                    try
                    {
                        referencedAssembly = Assembly.Load(assemblyName);
                    }
                    catch
                    {
                        referencedAssembly = null;
                    }

                    if (referencedAssembly == null)
                    {
                        string fullPathName = Path.Combine(workingDirectory, assemblyName.Name + ".dll");
                        referencedAssembly = LoadAssembly(fullPathName);
                    }

                    if (referencedAssembly == null)
                    {
                        string fullPathName = Path.Combine(workingDirectory, assemblyName.Name + ".exe");
                        referencedAssembly = LoadAssembly(fullPathName);
                    }

                    if (referencedAssembly != null)
                    {
                        AddAssemblyToManifest(referencedAssembly);
                    }
                    else
                    {
                        textOutput.Info.WriteLine("\nUnable to load referenced assembly '{0}'\n", name);
                    }
                }
            }
        }

        private Assembly LoadAssembly(string fullPathName)
        {
            Assembly assembly;
            try
            {
                assembly = Assembly.LoadFrom(fullPathName);
            }
            catch (Exception)
            {
                assembly = null;
            }
            return assembly;
        }
    }
}